#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include "boost/tuple/tuple.hpp"
#include <SDL/SDL.h>
#include "guru_meditation.h"
#include "fmemory.h"
#include "heroz.hh"
#include "heroz_basic.hh"

using std::cout;
using std::cerr;
using std::endl;

typedef HeroZ (*herofunptr)(HeroZ *);

HeroZ *get_levels(HeroZ *heroz) {
  assert(heroz);
  heroz->levels = new Levels;
  if(!heroz->levels) {
    throw boost::tuple<Uint32, void*>(GM_SS_Level | GM_GE_NoMemory, (void*)&get_levels);
  }
  string fpath(heroz->datapath);
  fpath += "/levels";
  heroz->levels->load(fpath.c_str());
  heroz->font->print_at(40, heroz->infopos += 16, "\\ Level loaded...");
  SDL_Flip(heroz->display);
  return heroz;
}

HeroZ *get_memory(HeroZ *heroz) {
  assert(heroz);
  heroz->memory = static_cast<unsigned char*>(falloc(1L<<16));
  if(!heroz->memory) {
    throw boost::tuple<Uint32, void*>(GM_SS_Exec | GM_GE_NoMemory, (void*)&get_memory);
  }
  assert(sizeof(ScreenCharacters) < 0x1000);
  heroz->screen = (ScreenCharacters *)(heroz->memory + 0x1000);
  heroz->txtscreen = (ScreenCharacters *)(heroz->memory + 0x2000);
  heroz->txtscreen->clear();
  return heroz;
}

HeroZ *get_input(HeroZ *heroz) {
  assert(heroz);
  heroz->input = new Input();
  if(!heroz->input) {
    throw boost::tuple<Uint32, void*>(GM_SS_KeyBoard | GM_GE_OpenLib, (void*)&get_input);    
  }
  return heroz;
}

HeroZ *get_muzaks(HeroZ *heroz) {
  assert(heroz);
  heroz->muzaks = init_sound(heroz->datapath);
  if(heroz->muzaks.empty()) {
    throw boost::tuple<Uint32, void*>(GM_SS_Audio | GM_GE_OpenLib, (void*)&get_muzaks);    
  } else {
#ifdef DEBUG
    cout << "Mods loaded: " << heroz->muzaks.size() << endl;
#endif
  }
  heroz->font->print_at(40, heroz->infopos += 16, "\\ Mods loaded...");
  SDL_Flip(heroz->display);
  return heroz;
}

HeroZ *get_font(HeroZ *heroz) {
  assert(heroz);
  assert(heroz->display);
  heroz->font = new HFont(heroz->datapath, heroz->display);
  if(!heroz->font) {
    throw boost::tuple<Uint32, void*>(GM_SS_Graphics | GM_GE_OpenLib, (void*)&get_font);
  }
  heroz->font->print_at(40, heroz->infopos += 16, "\\ HFont loaded...");
  SDL_Flip(heroz->display);
  return heroz;
}

HeroZ *get_gfx(HeroZ *heroz) {
  assert(heroz);
  assert(heroz->display);
  assert(heroz->font);
  heroz->gfx = new HerozGraphics(heroz->display, heroz->datapath);
  if(!heroz->gfx) {
    throw boost::tuple<Uint32, void*>(GM_SS_Graphics | GM_GE_OpenLib, (void*)&get_gfx);
  }
  heroz->font->print_at(40, heroz->infopos += 16, "\\ Sprites loaded...");
  SDL_Flip(heroz->display);
  heroz->screen_buffer = SDL_CreateRGBSurface(SDL_HWSURFACE, heroz->display->w, heroz->display->h, 8, 0, 0, 0, 0);
  if(!heroz->screen_buffer) {
    throw boost::tuple<Uint32, void*>(GM_SS_Graphics | GM_GE_OpenLib | 1, (void*)&get_gfx);
  } else {
    SDL_Palette *palette = heroz->font->gfxfont->format->palette;
    SDL_SetPalette(heroz->screen_buffer, SDL_LOGPAL, palette->colors, 0, palette->ncolors);
    heroz->font->print_at(40, heroz->infopos += 16, "\\ Screen buffer created...");
    SDL_Flip(heroz->display);
  }
  return heroz;
}

HeroZ *get_basic(HeroZ *heroz) {
  assert(heroz);
  assert(heroz->display);
  assert(heroz->font);
  assert(heroz->gfx);
  try {
    heroz->basic = load_program(heroz->datapath, heroz);
  }
  catch(const std::exception &excp) {
    cerr << "Error while loading basic code: " << excp.what() << endl;
    throw boost::tuple<Uint32, void*>(0x40000000L | GM_GE_OpenLib, (void*)&get_basic);
  }
  heroz->font->print_at(40, heroz->infopos += 16, "\\ Code loaded...");
  SDL_Flip(heroz->display);
  return heroz;
}

void clean_up(HeroZ *heroz) {
  heroz->quit = 1;
  SDL_Delay(1272);
  //if(heroz->display_thread) SDL_KillThread(heroz->display_thread);
  quit_program(heroz->basic);
  cleanup_sound(heroz->muzaks);
  delete heroz->gfx;
  delete heroz->font;
  delete heroz->input;
  SDL_FreeSurface(heroz->screen_buffer);
  free((void*)heroz->datapath);
  ffree(heroz->memory);
}

HeroZ *init_engine(const char *path) {
  HeroZ *heroz;

  heroz = static_cast<HeroZ*>(falloc(sizeof(HeroZ)));
  if(!heroz) {
    cerr << "No memory for HeroZ!\n";
    return NULL;
  } else {
    heroz->datapath = strdup(path);
    heroz->infopos = 30;
  }
  if(SDL_Init(SDL_INIT_EVENTTHREAD | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_TIMER) != 0) {
    cerr << "Fatal error! Can not initialise SDL: " << SDL_GetError() << ".\n";
    return NULL;
  } else {
    std::atexit(SDL_Quit);
    SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE); //Ignore all mouse movements...
  }
  if((heroz->display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
    cerr << "No video mode: " << SDL_GetError() << endl;
    return NULL;
  }
  try {
    heroz = get_font(get_memory(heroz));
    heroz = get_gfx(heroz);
    heroz = get_input(heroz);
    heroz->screens = load_screens(heroz->datapath);
    heroz->font->print_at(40, heroz->infopos += 16, "\\ Screens loaded...");
    SDL_Flip(heroz->display);
    heroz = get_levels(heroz);
    assert(heroz->screens.size() > 0);
    heroz = get_basic(heroz);
    assert(heroz->basic);
    heroz = get_muzaks(heroz);
    assert(heroz->muzaks.size() > 0);
    assert(heroz->input);
  }
  catch(const boost::tuple<Uint32, void*>&excp) {
    fprintf(stderr, "Initialisation failure! Subsystem=%08X, ptr=%08lX.\n", excp.get<0>(), (unsigned long int)(excp.get<1>()));
    guru_meditation(GM_FLAGS_DEADEND, excp.get<0>(), excp.get<1>());
    return NULL;
  }
  catch(const std::exception &excp) {
    cerr << "Initialisation failure: " << excp.what() << endl;
    guru_meditation(GM_FLAGS_DEADEND, GM_SS_Exec | GM_GE_ProcCreate, (void*)&init_engine);
    //clean_up(heroz);
    return NULL;
  }
  return heroz;
}

int display_thread(void *heroptr) {
  Uint32 col;
#ifndef NDEBUG
  FILE *outf;
#endif
  unsigned long display_start_ticks;
  HeroZ *heroz = (HeroZ *)heroptr;
  SDL_Surface *hwdisplay = SDL_GetVideoSurface();

  assert(heroz->gfx);
  col = SDL_MapRGB(hwdisplay->format, 0, 0xFF, 0);
#ifndef NDEBUG
  outf = fopen("/tmp/frame_information.txt", "w");
#endif
  heroz->last_ticks = SDL_GetTicks();
  display_start_ticks = heroz->last_ticks;
  do {
    Uint32 current_ticks = SDL_GetTicks();
    heroz->mvmr = (current_ticks - heroz->last_ticks) / 1000.0;
    SDL_FillRect(hwdisplay, NULL, col);
    //draw_screen(heroz->screen_buffer, heroz->font->gfxfont, heroz->last_ticks / 137, heroz->screen);
    draw_screen(heroz);
    SDL_BlitSurface(heroz->screen_buffer, NULL, SDL_GetVideoSurface(), NULL);
    heroz->gfx->draw_sprites();
    SDL_Flip(hwdisplay);
#ifndef NDEBUG
    if(outf) {
      fprintf(outf, "%08lX %ld (%+ld) %ld\n", (unsigned long int)heroz->displayed_frame_number, (long)current_ticks, (long)current_ticks - heroz->last_ticks, (long)SDL_GetTicks() - current_ticks);
    }
#endif
    unsigned long needed_delay = (++heroz->displayed_frame_number * 20) - (SDL_GetTicks() - display_start_ticks);
    SDL_Delay(needed_delay > 22 ? 15 : needed_delay);
    heroz->last_ticks = current_ticks;
  } while(!heroz->quit);
  SDL_LockSurface(hwdisplay);
  for(short int x = 0; x < hwdisplay->w; ++x) {
    for(short int y = 0; y < hwdisplay->h; ++y) {
      if((x & 1) == (y & 1)) _PutPixel(hwdisplay, x, y, 0xFFL);
    }
  }
  SDL_UnlockSurface(hwdisplay);
  SDL_Flip(hwdisplay);
#ifndef NDEBUG
  if(outf) fclose(outf);
#endif
  return 0;
}

void init_threads(HeroZ *heroz) {
  assert(heroz);
  assert(!heroz->display_thread);
  heroz->display_thread = SDL_CreateThread(&display_thread, heroz);
  if(!heroz->display_thread) guru_meditation(GM_FLAGS_DEADEND | GM_FLAGS_AUTOTIMEOUT, GM_SS_Exec | GM_GE_ProcCreate, (void*)&init_threads);
}
